#include <openssl/ssl.h>
#include <openssl/err.h>

#include <string>
#include <vector>
#include <functional>

#define MY_MAX(x,y) ((x)>(y)?(x):(y))
#define MY_MIN(x,y) ((x)>(y)?(y):(x))

#define MY_BUFFER_SIZE 128

#ifdef MY_OS_WINDOWS
  #include <winsock2.h>
  #include <ws2tcpip.h>
  #define inet_aton(str,addr) inet_pton(AF_INET, str, addr)
  typedef HANDLE task_handle_t;
  static int fd_is_valid(int fd) {
    char buffer[1];
    recv(fd, buffer, 1, MSG_PEEK);
    return (WSAECONNRESET != WSAGetLastError());
  }
  #define my_delay(ms) Sleep(ms)
#endif

#ifdef MY_OS_LINUX
  #include <fcntl.h>
  #include <errno.h>
  #include <pthread.h>
  #include <sys/socket.h>
  #include <netinet/in.h>
  #define closesocket close
  typedef pthread_t task_handle_t;
  typedef int SOCKET;
  static int fd_is_valid(int fd) {
    return fcntl(fd, F_GETFD, 0) != -1 || errno != EBADF;
  }
  #define my_delay(ms) sleep(ms/1000)
#endif

#define MY_LOG(tag, format, ...) printf("[%s] " format "\n", tag, ##__VA_ARGS__)
#define TAG_CLIENT "CLIENT"
#define TAG_SERVER "SERVER"
#define TAG_MAIN   "MAIN"

struct str_t {
  const char *data;
  uint32_t size;
};

struct url_t {
  const char *url;
  str_t protocol;
  str_t hostname;
  str_t port;
  str_t path;
  str_t search;
  str_t hash;
};

// http://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#SomewhereInTheDocument
static bool parse_url(const char *url, url_t *val) {
  if (!url || !*url || !val) return false;
  memset(val, 0, sizeof(url_t));
  val->url = url;
  val->protocol.data = url;
  url = strstr(url, "://");
  if (!url) return false;
  val->protocol.size = url - val->protocol.data;
  val->hostname.data = url + 3;
  url = strchr(val->hostname.data, ':');
  if (url) {
    val->hostname.size = url - val->hostname.data;
    val->port.data = url + 1;
    url = strchr(val->port.data, '/');
    if (url) {
      val->port.size = url - val->port.data;
      val->path.data = url;
    } else {
      val->port.size = strlen(val->port.data);
      return true;
    }
  } else {
    url = strchr(val->hostname.data, '/');
    if (url) {
      val->hostname.size = url - val->hostname.data;
      val->path.data = url;
    } else {
      val->hostname.size = strlen(val->hostname.data);
      return true;
    }
  }
  url = strchr(val->path.data, '?');
  if (url) {
    val->path.size = url - val->path.data;
    val->search.data = url + 1;
  } else {
    val->path.size = strlen(val->path.data);
    return true;
  }
  url = strchr(val->search.data, '#');
  if (url) {
    val->search.size = url - val->search.data;
    val->hash.data = url + 1;
    val->hash.size = strlen(val->hash.data);
  } else {
    val->search.size = strlen(val->search.data);
    return true;
  }
  return true;
}

static void show_url(const url_t *url) {
  if (!url) return;
  printf("---------- show_url begin ----------\n");
  printf("url: %s\n", url->url);
  printf("protocol: %.*s\n", url->protocol.size, url->protocol.data);
  printf("hostname: %.*s\n", url->hostname.size, url->hostname.data);
  printf("port: %.*s\n", url->port.size, url->port.data);
  printf("path: %.*s\n", url->path.size, url->path.data);
  printf("search: %.*s\n", url->search.size, url->search.data);
  printf("hash: %.*s\n", url->hash.size, url->hash.data);
  printf("---------- show_url end ----------\n");
}

static void show_hex(const char *data, int len) {
  for (int i = 0; i < len; ++i) {
    if (i % 4 == 0) {
      if (i % 8 == 0) {
        if (i) printf("| \n");
      }
      else printf("| ");
    }
    printf("%02X ", (unsigned char)data[i]);
  }
  printf("\n");
}

static bool parse_url(const char *urlstr, std::string *hostname, std::string *path, uint16_t *port) {
  url_t url = { 0 };
  if (!parse_url(urlstr, &url)) return false;

  std::string strport;

  hostname->assign(url.hostname.data, url.hostname.size);
  path->assign(url.path.data, url.path.size);
  strport.assign(url.port.data, url.port.size);

  if (hostname->empty()) return false;
  if (strport.empty()) {
    strport = 0 == strncmp("https", url.protocol.data, url.protocol.size)
        ? "443" : "80";
  }
  if (path->empty()) *path = "/";
  *port = atoi(strport.c_str());

  return true;
}

bool client(const char *hostname, uint16_t port, void *udata
    , std::function<bool(SSL *ssl, void *udata)> callback) {
  bool ret = false;
  SSL *ssl = 0;
  SSL_CTX *ctx = 0;
  SOCKET fd = -1;
  sockaddr_in addr = { 0 };
  hostent *p = 0;

  SSL_library_init();
  OpenSSL_add_all_algorithms();
  SSL_load_error_strings();

  ctx = SSL_CTX_new(SSLv23_client_method());
  if (!ctx) {
    MY_LOG(TAG_CLIENT, "SSL_CTX_new error.");
    goto lb_err;
  }
  SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, 0);

  ssl = SSL_new(ctx);
  if (!ssl) {
    MY_LOG(TAG_CLIENT, "SSL_new error.");
    goto lb_err;
  }
  
  fd = socket(AF_INET, SOCK_STREAM, 0);
  if (fd < 0) {
    MY_LOG(TAG_CLIENT, "create socket error.");
    goto lb_err;
  }

  addr.sin_addr.S_un.S_addr = 0;
	addr.sin_port = htons(0);
	addr.sin_family = AF_INET;
	if (bind(fd, (const sockaddr*)&addr, sizeof(SOCKADDR_IN)) != NOERROR) {
    MY_LOG(TAG_CLIENT, "bind error.");
    goto lb_err;
  }

	p = gethostbyname(hostname);
  if (!p) {
    MY_LOG(TAG_CLIENT, "gethostbyname error.");
    goto lb_err;
  }

  addr.sin_family = p->h_addrtype;
  addr.sin_addr.S_un.S_addr = *(ULONG*)(p->h_addr_list[0]);
  addr.sin_port = htons(port);

  if (connect(fd, (sockaddr*)&addr, sizeof(addr)) != NOERROR) {
    MY_LOG(TAG_CLIENT, "connect error.");
    goto lb_err;
  }

  //MY_LOG(TAG_CLIENT, "connect ok.");

  if (SSL_set_fd(ssl, fd) != SSL_SUCCESS) {
    MY_LOG(TAG_CLIENT, "SSL_set_fd error.");
    goto lb_err;
  }
  if (SSL_connect(ssl) != SSL_SUCCESS) {
    MY_LOG(TAG_CLIENT, "SSL_connect error.");
    goto lb_err;
  }
  //MY_LOG(TAG_CLIENT, "connected with %s encryption.", SSL_get_cipher(ssl));

  if (callback)
    ret = callback(ssl, udata);

  ret = true;
 lb_err:
  if (ssl) {
    SSL_shutdown(ssl);
    SSL_free(ssl);
  }
  if (fd) closesocket(fd);
  if (ctx) SSL_CTX_free(ctx);
  return ret;
}

bool http_get(const char *urlstr, void *udata, std::function<void(const char *data, int32_t size, void *udata)> callback) {
  std::string hostname, path;
  uint16_t port;
  if (!parse_url(urlstr, &hostname, &path, &port))
    return false;

  char header[1024 * 8];
	sprintf(header,
      "GET %s HTTP/1.1 \r\n"
      "Host: %s \r\n"
      "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0 \r\n"
      "Accept-Type: */* \r\n"
      "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 \r\n"
      "Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 \r\n"
      "Connection: Close \r\n\r\n"
    , path.c_str()
    , hostname.c_str());

  return client(hostname.c_str(), port, udata
    , [&] (SSL *ssl, void *udata) -> bool {
      int header_len = strlen(header);
      int ret = SSL_write(ssl, header, header_len);
      if (ret != header_len) return false;
      if (!callback) return true;

      char buf[MY_BUFFER_SIZE];
      while (true) {
        ret = SSL_read(ssl, buf, MY_BUFFER_SIZE);
        if (ret <= 0) break;
        callback(buf, ret, udata);
      }
      return true;
    });
}

typedef struct {
  SOCKET fd;
  SSL *ssl;
} conn_t;
bool server(const char *cert_file, const char *key_file, uint16_t port, void *udata
    , std::function<bool(conn_t *conn, void *udata)> callback) {
  bool ret = false;
  SSL_CTX *ctx = 0;
  SOCKET fd_listen;
  sockaddr_in addr = { 0 };

  if (!cert_file || !key_file) return false;
  if (!callback) return false;

  SSL_library_init();
  OpenSSL_add_all_algorithms();
  SSL_load_error_strings();

  ctx = SSL_CTX_new(SSLv23_server_method());
  if (!ctx) {
    MY_LOG(TAG_SERVER, "ssl create ctx error.");
    goto lb_err;
  }

  if (SSL_CTX_use_certificate_file(ctx, cert_file, SSL_FILETYPE_PEM) != SSL_SUCCESS) {
    MY_LOG(TAG_SERVER, "ssl load cert file error.");
    goto lb_err;
  }
  if (SSL_CTX_use_PrivateKey_file(ctx, key_file, SSL_FILETYPE_PEM) != SSL_SUCCESS) {
    MY_LOG(TAG_SERVER, "ssl load key file error.");
    goto lb_err;
  }
  if (SSL_CTX_check_private_key(ctx) != SSL_SUCCESS) {
    MY_LOG(TAG_SERVER, "ssl check key error.");
    goto lb_err;
  }

  SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, 0);

  fd_listen = socket(AF_INET, SOCK_STREAM, 0);
  if (fd_listen < 0) {
    MY_LOG(TAG_SERVER, "create socket error.");
    goto lb_err;
  }

  addr.sin_family = AF_INET;
  addr.sin_addr.S_un.S_addr = INADDR_ANY;
  addr.sin_port = htons(port);
  if (bind(fd_listen, (const sockaddr*)&addr, sizeof(SOCKADDR_IN)) != NOERROR) {
    MY_LOG(TAG_SERVER, "bind error.");
    goto lb_err;
  }

  if (listen(fd_listen, 5) != NOERROR) {
    MY_LOG(TAG_SERVER, "listen error.");
    goto lb_err;
  }
  
  MY_LOG(TAG_SERVER, "listen in 0.0.0.0:%d.", port);

  bool quit = false;
  while (!quit) {
    SSL *ssl = 0;
    bool is_ssl = false;
    char handshake[8];
    conn_t conn = { 0 };

    struct sockaddr_in addr_from;
    socklen_t addr_from_len = sizeof(addr_from);
    SOCKET fd_conn = accept(fd_listen, (struct sockaddr *)&addr_from, &addr_from_len);
    if (fd_conn < 0) {
      MY_LOG(TAG_SERVER, "accept error.");
      goto lb_finish;
    }

    int ret = recv(fd_conn, (char*)&handshake, sizeof(handshake), MSG_PEEK);
    if (ret < 0) {
      MY_LOG(TAG_SERVER, "recv error.");
      goto lb_finish;
    } else if (ret == 0) {
      MY_LOG(TAG_SERVER, "close fd %d.", (int)fd_conn);
      goto lb_finish;
    } else {
      if ((unsigned char)handshake[0] == 0x16 // content type
          && (unsigned char)handshake[5] == 0x01) { // handshake type
        is_ssl = true;
      }
      MY_LOG(TAG_SERVER, "check is ssl: %d", is_ssl);
      show_hex((const char*)&handshake, ret);
    }

    if (is_ssl) {
      ssl = SSL_new(ctx);
      if (!ssl) {
        MY_LOG(TAG_SERVER, "SSL_new error.");
        goto lb_finish;
      }
      if (SSL_set_fd(ssl, fd_conn) != SSL_SUCCESS) {
        MY_LOG(TAG_SERVER, "SSL_set_fd error.");
        goto lb_finish;
      }
      if (SSL_accept(ssl) != SSL_SUCCESS) {
        MY_LOG(TAG_SERVER, "SSL_accept error.");
        goto lb_finish;
      }
      conn.fd = fd_conn;
      conn.ssl = ssl;
    } else {
      conn.fd = fd_conn;
    }

    quit = !callback(&conn, udata);

   lb_finish:
    if (ssl) {
      SSL_shutdown(ssl);
      SSL_free(ssl);
    }
    if (fd_conn >= 0) closesocket(fd_conn);
  }

 lb_err:
  if (fd_listen >= 0) closesocket(fd_listen);
  if (ctx) SSL_CTX_free(ctx);
  return ret;
}

bool tcp_echo(const char *cert_file, const char *key_file, uint16_t port) {
  return server(cert_file, key_file, port, 0
    , [](conn_t *conn, void *udata) -> bool {
      char buf[MY_BUFFER_SIZE];
      while (true) {
        int ret = SSL_read(conn->ssl, buf, MY_BUFFER_SIZE);
        if (ret <= 0) break;
        printf("%.*s", ret, buf);
        SSL_write(conn->ssl, buf, ret);
      }
      return true;
    });
}

str_t parse_request(const char *req) {
  str_t ret = { 0 };
  if (!req || !*req) return ret;
  // GET /?url=https://www.baidu.com?a=1&b=2 HTTP/1.1
  if (strncmp(req, "GET /", 5) != 0) return ret;
  const char *bp = req + 4;
  const char *ep = strchr(bp, ' ');
  if (!ep) return ret;
  ret.data = bp;
  ret.size = ep - bp;
  return ret;
}

int user_main(int argc, char *argv[]) {
  if (argc != 5) {
    MY_LOG(TAG_MAIN, "#> https <cert file> <cert key> <port> <dest>");
    MY_LOG(TAG_MAIN, "such as:  https server.cert server.key 8888 https://s.dzlua.top");
    return 0;
  }

  const char *crt = argv[1];
  const char *key = argv[2];
  uint16_t port = atoi(argv[3]);
  const char *dest = argv[4];

  bool ok = server(crt, key, port, (void*)dest
    , [](conn_t *conn, void *udata) -> bool {
      std::string strRecv;

      printf("---------- begin ----------\n");
      printf("is ssl: %s\n", (conn->ssl ? "true" : "false"));
      if (conn->ssl) {
        std::string path;
        char buf[MY_BUFFER_SIZE];
        while (true) {
          int ret = SSL_read(conn->ssl, buf, MY_BUFFER_SIZE);
          if (ret <= 0) break;
          strRecv.append(buf, ret);
          printf("%.*s", ret, buf);
          str_t tmp = parse_request(strRecv.c_str());
          path.assign(tmp.data, tmp.size);
          if (!path.empty()) break;
        }

        printf("\n----------\n");
        path = (const char*)udata + path;
        http_get(path.c_str(), conn->ssl
          , [](const char *data, int32_t size, void *udata) {
            printf("%.*s", size, data);
            SSL_write((SSL*)udata, data, size);
          });
      } else {
        return true;
      }

      printf("\n---------- end ----------\n");
      return true;
    });

  return 0;
}